/**
 * Copyright 2010-present Facebook.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook;

import android.net.Uri;
import android.os.Bundle;
import bolts.AppLink;
import bolts.AppLinkResolver;
import bolts.Continuation;
import bolts.Task;
import com.facebook.model.GraphObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.*;

/**
 * Provides an implementation for the {@link AppLinkResolver AppLinkResolver} interface that uses the Facebook App Link
 * index to solve App Links, given a Url. It also provides an additional helper method that can resolve multiple App
 * Links in a single call.
 */
public class FacebookAppLinkResolver implements AppLinkResolver
{

  private static final String APP_LINK_ANDROID_TARGET_KEY = "android";
  private static final String APP_LINK_WEB_TARGET_KEY = "web";
  private static final String APP_LINK_TARGET_PACKAGE_KEY = "package";
  private static final String APP_LINK_TARGET_CLASS_KEY = "class";
  private static final String APP_LINK_TARGET_APP_NAME_KEY = "app_name";
  private static final String APP_LINK_TARGET_URL_KEY = "url";
  private static final String APP_LINK_TARGET_SHOULD_FALLBACK_KEY = "should_fallback";

  private final HashMap<Uri, AppLink> cachedAppLinks = new HashMap<Uri, AppLink>();

  /**
   * Asynchronously resolves App Link data for the passed in Uri
   *
   * @param uri Uri to be resolved into an App Link
   * @return A Task that, when successful, will return an AppLink for the passed in Uri. This may be null if no App
   * Link data was found for this Uri.
   * In the case of general server errors, the task will be completed with the corresponding error.
   */
  public Task<AppLink> getAppLinkFromUrlInBackground(final Uri uri)
  {
    ArrayList<Uri> uris = new ArrayList<Uri>();
    uris.add(uri);

    Task<Map<Uri, AppLink>> resolveTask = getAppLinkFromUrlsInBackground(uris);

    return resolveTask.onSuccess(new Continuation<Map<Uri, AppLink>, AppLink>()
    {
      @Override
      public AppLink then(Task<Map<Uri, AppLink>> resolveUrisTask) throws Exception
      {
        return resolveUrisTask.getResult().get(uri);
      }
    });
  }

  /**
   * Asynchronously resolves App Link data for multiple Urls
   *
   * @param uris A list of Uri objects to resolve into App Links
   * @return A Task that, when successful, will return a Map of Uri->AppLink for each Uri that was successfully
   * resolved into an App Link. Uris that could not be resolved into App Links will not be present in the Map.
   * In the case of general server errors, the task will be completed with the corresponding error.
   */
  public Task<Map<Uri, AppLink>> getAppLinkFromUrlsInBackground(List<Uri> uris)
  {
    final Map<Uri, AppLink> appLinkResults = new HashMap<Uri, AppLink>();
    final HashSet<Uri> urisToRequest = new HashSet<Uri>();
    StringBuilder graphRequestFields = new StringBuilder();

    for (Uri uri : uris)
    {
      AppLink appLink = null;
      synchronized (cachedAppLinks)
      {
        appLink = cachedAppLinks.get(uri);
      }

      if (appLink != null)
      {
        appLinkResults.put(uri, appLink);
      }
      else
      {
        if (!urisToRequest.isEmpty())
        {
          graphRequestFields.append(',');
        }
        graphRequestFields.append(uri.toString());
        urisToRequest.add(uri);
      }
    }

    if (urisToRequest.isEmpty())
    {
      return Task.forResult(appLinkResults);
    }

    final Task<Map<Uri, AppLink>>.TaskCompletionSource taskCompletionSource = Task.create();

    Bundle appLinkRequestParameters = new Bundle();
    appLinkRequestParameters.putString("type", "al");
    appLinkRequestParameters.putString("ids", graphRequestFields.toString());
    appLinkRequestParameters.putString(
        "fields",
        String.format("%s,%s", APP_LINK_ANDROID_TARGET_KEY, APP_LINK_WEB_TARGET_KEY));

    Request appLinkRequest = new Request(
        null, /* Session */
        "", /* Graph path */
        appLinkRequestParameters, /* Query parameters */
        null, /* HttpMethod */
        new Request.Callback()
        { /* Callback */
          @Override
          public void onCompleted(Response response)
          {
            FacebookRequestError error = response.getError();
            if (error != null)
            {
              taskCompletionSource.setError(error.getException());
              return;
            }

            GraphObject responseObject = response.getGraphObject();
            JSONObject responseJson = responseObject != null ? responseObject.getInnerJSONObject() : null;
            if (responseJson == null)
            {
              taskCompletionSource.setResult(appLinkResults);
              return;
            }

            for (Uri uri : urisToRequest)
            {
              String uriString = uri.toString();
              if (!responseJson.has(uriString))
              {
                continue;
              }

              JSONObject urlData = null;
              try
              {
                urlData = responseJson.getJSONObject(uri.toString());
                JSONArray rawTargets = urlData.getJSONArray(APP_LINK_ANDROID_TARGET_KEY);

                int targetsCount = rawTargets.length();
                List<AppLink.Target> targets = new ArrayList<AppLink.Target>(targetsCount);

                for (int i = 0; i < targetsCount; i++)
                {
                  AppLink.Target target = getAndroidTargetFromJson(rawTargets.getJSONObject(i));
                  if (target != null)
                  {
                    targets.add(target);
                  }
                }

                Uri webFallbackUrl = getWebFallbackUriFromJson(uri, urlData);
                AppLink appLink = new AppLink(uri, targets, webFallbackUrl);

                appLinkResults.put(uri, appLink);
                synchronized (cachedAppLinks)
                {
                  cachedAppLinks.put(uri, appLink);
                }
              }
              catch (JSONException e)
              {
                // The data for this uri was missing or badly formed.
                continue;
              }
            }

            taskCompletionSource.setResult(appLinkResults);
          }
        });

    appLinkRequest.executeAsync();

    return taskCompletionSource.getTask();
  }

  private static AppLink.Target getAndroidTargetFromJson(JSONObject targetJson)
  {
    String packageName = tryGetStringFromJson(targetJson, APP_LINK_TARGET_PACKAGE_KEY, null);
    if (packageName == null)
    {
      // Package name is mandatory for each Android target
      return null;
    }
    String className = tryGetStringFromJson(targetJson, APP_LINK_TARGET_CLASS_KEY, null);
    String appName = tryGetStringFromJson(targetJson, APP_LINK_TARGET_APP_NAME_KEY, null);
    String targetUrlString = tryGetStringFromJson(targetJson, APP_LINK_TARGET_URL_KEY, null);
    Uri targetUri = null;
    if (targetUrlString != null)
    {
      targetUri = Uri.parse(targetUrlString);
    }

    return new AppLink.Target(packageName, className, targetUri, appName);
  }

  private static Uri getWebFallbackUriFromJson(Uri sourceUrl, JSONObject urlData)
  {
    // Try and get a web target. This is best effort. Any failures results in null being returned.
    try
    {
      JSONObject webTarget = urlData.getJSONObject(APP_LINK_WEB_TARGET_KEY);
      boolean shouldFallback = tryGetBooleanFromJson(webTarget, APP_LINK_TARGET_SHOULD_FALLBACK_KEY, true);
      if (!shouldFallback)
      {
        // Don't use a fallback url
        return null;
      }

      String webTargetUrlString = tryGetStringFromJson(webTarget, APP_LINK_TARGET_URL_KEY, null);
      Uri webUri = null;
      if (webTargetUrlString != null)
      {
        webUri = Uri.parse(webTargetUrlString);
      }

      // If we weren't able to parse a url from the web target, use the source url
      return webUri != null ? webUri : sourceUrl;
    }
    catch (JSONException e)
    {
      // If we were missing a web target, just use the source as the web url
      return sourceUrl;
    }
  }

  private static String tryGetStringFromJson(JSONObject json, String propertyName, String defaultValue)
  {
    try
    {
      return json.getString(propertyName);
    }
    catch (JSONException e)
    {
      return defaultValue;
    }
  }

  private static boolean tryGetBooleanFromJson(JSONObject json, String propertyName, boolean defaultValue)
  {
    try
    {
      return json.getBoolean(propertyName);
    }
    catch (JSONException e)
    {
      return defaultValue;
    }
  }
}
